﻿namespace Gunningine
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Diagnostics;

    public abstract class Entity : IEqualityComparer<Entity>, IDisposable
    {
        /// <summary>
        /// This is used to give the Entity a unique id for comparison when
        /// storing in collections.
        /// </summary>
        private static int nextId = 1;

        private int id;

        private IEntityContainer container;

        private IAlignmentTester alignmentTester;

        private Dictionary<string, EntityState> states = new Dictionary<string, EntityState>();

        private WorldState worldState;

        private Battery battery = null;

        /// <summary>
        /// Acceleration grid.
        /// </summary>
        private EntityGrid entityGrid;

        /// <summary>
        /// Core
        /// </summary>
        private ResourceManager resourceMgr;

        /// <summary>
        /// Initial state
        /// </summary>
        internal string InitialState { get; private set; }

        /// <summary>
        /// Current model.
        /// </summary>
        internal Model Model { get; private set; }

        /// <summary>
        /// Is this Entity current doing stuff?
        /// </summary>
        internal bool Alive { get; private set; }

        /// <summary>
        /// Current state.
        /// </summary>
        internal string State { get; private set; }

        /// <summary>
        /// Size of entity.
        /// </summary>
        internal float Size
        {
            get { return this.Model.Size; }
        }

        ///
        /// Accessible to scripting.
        ///

        protected EntityScriptingCore Core { get; private set; }

        /// <summary>
        /// Name of entity (type).
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Position.  A 3d vector, even though this is primarily a 2d game.  Generally
        /// the z-component will be set to zero, although there may be circumstances where
        /// it is not.
        /// </summary>
        public Vector3 Position { get; private set; }

        /// <summary>
        /// Facing direction.
        /// </summary>
        public Vector3 Direction { get; private set; }

        /// <summary>
        /// Angle the entity is facing, on X/Y plane.
        /// </summary>
        public float FacingAngle
        {
            get { return 90.0f - MathsUtils.RadToDeg(Math.Atan2(this.Direction.Y, this.Direction.X)); }
        }

        /// <summary>
        /// Speed which entity moves at, in units per second.
        /// </summary>
        public float MoveSpeed { get; protected set; }

        /// <summary>
        /// Speed which entity turns at, in degrees per second.
        /// </summary>
        public float TurnSpeed { get; protected set; }

        /// <summary>
        /// Which other entities this entity is hostile to.
        /// </summary>
        public int Alignment { get; protected set; }

        /// <summary>
        /// Constructor.
        /// 
        /// This must be public because it is called via 'base' in scripted entities.
        /// </summary>
        public Entity()
        {
            this.id = Entity.nextId++;
            this.Alive = false;
            this.State = null;
            this.Alignment = -1;

            this.MoveSpeed = 10.0f;
            this.TurnSpeed = 90.0f;

            this.Core = new EntityScriptingCore();
        }

        /// <summary>
        /// Constructor.
        /// 
        /// This must be public because it is called via 'base' in scripted entities.
        /// </summary>
        /// <param name="name">Entity name</param>
        /// <param name="position">position.</param>
        /// <param name="direction">direction.</param>
        /// <param name="size">size of entity.</param>
        /// <param name="flags">flags specific to this entity.</param>
        /// <param name="entityGrid">acceleration grid for entities.</param>
        public Entity(string name, Vector3 position, Vector3 direction, string initialState, WorldState worldState, int alignment)
            : this()
        {
            this.Name = name;
            this.Position = position;
            this.Direction = direction;
            this.InitialState = initialState;

            this.worldState = worldState;
            this.Alignment = alignment;
        }

        /// <summary>
        /// Set core for scripting.
        /// </summary>
        /// <param name="resourceManager"></param>
        internal void SetCore(ResourceManager resourceMgr)
        {
            this.resourceMgr = resourceMgr;
        }

        /// <summary>
        /// This activates an entity after we have set it up.  We do it like this so we
        /// can easily reuse Entities, rather than having to allocate a new one each time,
        /// which will be slow for large numbers of them.
        /// </summary>
        internal void Activate(EntityGrid entityGrid, Battery battery, IEntityContainer container, IAlignmentTester alignmentTester)
        {
            this.entityGrid = entityGrid;
            this.battery = battery;
            this.container = container;
            this.alignmentTester = alignmentTester;

            this.Alive = true;
            this.container.AddEntity(this);
            this.alignmentTester.AddEntity(this);
            this.entityGrid.AddEntity(this);
        }

        /// <summary>
        /// Deactivate.  Call after the Entity dies.
        /// </summary>
        internal void Deactivate()
        {
            this.Alive = false;
            this.container.RemoveEntity(this);
            this.alignmentTester.RemoveEntity(this);
            this.entityGrid.RemoveEntity(this);
            this.Dispose();
        }

        /// <summary>
        /// Update this entity for one frame.
        /// </summary>
        protected abstract void UpdateImpl();

        /// <summary>
        /// Can this entity see another?
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private bool SeesEntity(Entity target)
        {
            return this.Position.Distance(target.Position) < 50;
        }

        /// <summary>
        /// Update visual, and backend logic for entity.  This is done even when the entity
        /// is player-controlled.
        /// </summary>
        internal void UpdateLogic()
        {
            // Update animation.
            this.Model.Update(this.worldState.FrameTime, this, this.worldState);
        }

        /// <summary>
        /// Check for events.
        /// </summary>
        internal void CheckEvents()
        {
            // Which hostile entities can this entity see since frame?
            if (this.states[this.State].OnSee != null)
            {
                HashSet<Entity> seenEntities = this.alignmentTester.TestAlignedEntity(this, (a, b) => a.SeesEntity(b));
                if (seenEntities.Count > 0)
                    this.states[this.State].OnSee(seenEntities.ToArray());
            }

            // Has this entity been hit by a hostile entity since last frame?
            // ...
        }

        /// <summary>
        /// Update control for an entity, if not controlled by a player.
        /// </summary>
        /// <param name="worldState"></param>
        /// <param name="frameTime"></param>
        internal void UpdateControl()
        {
            if (this.states[this.State].OnUpdate != null)
            {
                this.states[this.State].OnUpdate(this.worldState);
            }
        }

        /// <summary>
        /// Render the entity.
        /// </summary>
        /// <param name="renderSystem">render system to use for rendering.</param>
        internal void Render(RenderSystem renderSystem)
        {
            renderSystem.PushTransform();
            renderSystem.Translate(this.Position);

            renderSystem.SetColour(1, 1, 1);

            Texture entityTexture = renderSystem.GetTexture(this.Model.Name + "-1");
            entityTexture.Bind();

            float rotateTarget = -MathsUtils.RadToDeg(Math.Atan2(this.Direction.Y, this.Direction.X));

            renderSystem.Rotate(rotateTarget, Vector3.NegativeUnitZ);
            renderSystem.Scale(0.2f, 0.2f, 0.2f);

            Shader shader = renderSystem.GetShader("EntityShader");
            shader.SetValueUniform("lerp", this.Model.Animation.Time * this.Model.Animation.TimeScale);

            Mesh entityMesh = renderSystem.GetMesh(this.Model.Name);
            entityMesh.Render(this.Model.Animation.Name, this.Model.Animation.CurFrame);

            entityTexture.Unbind();
            renderSystem.PopTransform();
        }

        ///
        /// Accessible to scripting.
        ///

        /// <summary>
        /// Set the model this entity will use.
        /// </summary>
        /// <param name="model"></param>
        public void SetModel(Model model)
        {
            this.Model = model;
            this.Model.Initialise(this.resourceMgr);
        }

        /// <summary>
        /// Set the animation this entity will use.
        /// </summary>
        /// <param name="model"></param>
        public void SetAnimation(string animation)
        {
            Debug.Assert(this.Model != null, String.Format("Could not set Entity '{0}' animation to '{1}': this.Model is null", this.Name, animation));

            if (this.Model.Animation.Name != animation)
            {
                this.Model.SetAnimation(animation);
            }
        }

        /// <summary>
        /// Set current state.
        /// </summary>
        /// <param name="state"></param>
        public void SetState(string state, params object[] args)
        {
            if (this.State != null)
                if (this.states[this.State].OnExit != null)
                    this.states[this.State].OnExit();

            this.State = state;

            if (this.states[this.State].OnEnter != null)
                this.states[this.State].OnEnter(args);
        }

        /// <summary>
        /// Register a new state.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="updateFunc"></param>
        public void RegisterState(
            string name,
            Action<object[]> enterFunc,
            Action exitFunc,
            Action<WorldState> updateFunc,
            Action<Entity[]> onSeeFunc)
        {
            if (this.states.ContainsKey(name))
                throw new GameException(String.Format("State '{0}' is already registered for entity '{1}'.", name, this.Name));

            EntityState state = new EntityState(name);
            state.SetEnterFunction(enterFunc);
            state.SetExitFunction(exitFunc);
            state.SetUpdateFunction(updateFunc);
            state.SetOnSeeFunction(onSeeFunc);

            this.states[name] = state;
        }

        /// <summary>
        /// Move entity.
        /// </summary>
        /// <param name="direction">direction (normalised).</param>
        public void Move(Vector3 direction)
        {
            Vector3 newPosition = this.Position + direction * this.MoveSpeed * this.worldState.FrameTime;
            this.entityGrid.MoveEntity(this, newPosition);
            this.Position = newPosition;
        }

        /// <summary>
        /// Move forward.
        /// </summary>
        public void MoveForward()
        {
            Move(this.Direction);
        }

        /// <summary>
        /// Move backward.
        /// </summary>
        public void MoveBackward()
        {
            Move(-this.Direction);
        }

        /// <summary>
        /// Turn towards the specified direction.
        /// </summary>
        /// <param name="direction"></param>
        public void TurnTowards(Vector3 direction)
        {
            if (direction.X * this.Direction.Y > direction.Y * this.Direction.X)
                TurnLeft();
            else
                TurnRight();
        }

        /// <summary>
        /// Turn the entity left.
        /// </summary>
        /// <param name="angle">angle to turn by.</param>
        public void TurnLeft()
        {
            // Negative axis because OpenGL is right-handed.
            this.Direction = Vector3.Rotate(this.Direction, this.TurnSpeed * this.worldState.FrameTime, Vector3.NegativeUnitZ);
        }

        /// <summary>
        /// Turn the entity right.
        /// </summary>
        /// <param name="angle">angle to turn by.</param>
        public void TurnRight()
        {
            // Negative axis because OpenGL is right-handed.
            this.Direction = Vector3.Rotate(this.Direction, -this.TurnSpeed * this.worldState.FrameTime, Vector3.NegativeUnitZ);
        }

        /// <summary>
        /// Turn the entity to face specified angle.
        /// </summary>
        /// <param name="angle">angle to turn to.</param>
        public void SetFacingAngle(float angle)
        {
            this.Direction = Vector3.Rotate(this.Direction, angle - this.FacingAngle, Vector3.NegativeUnitZ);
        }

        /// <summary>
        /// Emit an object.
        /// </summary>
        /// <param name="emitted"></param>
        public void Emit(Emitted emitted, Vector3 position)
        {
            emitted.Owner = this;
            emitted.Position = position;
            
            // Add to battery.
            this.battery.Add(emitted);
        }

        #region IEqualityComparer implementation

        public bool Equals(Entity a, Entity b)
        {
            return a.id == b.id;
        }

        public int GetHashCode(Entity entity)
        {
            return entity.id;
        }

        #endregion

        #region IDisposable implementation

        bool disposed = false;

        public void Dispose()
        {
            if (!this.disposed)
            {
                this.disposed = true;
            }
        }

        #endregion

    }
}
